package com.googlecode.curvedmetromaps.core.model.importer;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import com.googlecode.curvedmetromaps.core.model.MetroLine;
import com.googlecode.curvedmetromaps.core.model.MetroMap;
import com.googlecode.curvedmetromaps.core.model.MetroStation;

public class OsmMapParser extends MapParser {

	@Override
	public void parse(String resource, final MetroMap map) {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		DocumentBuilder builder;
		try {
			builder = factory.newDocumentBuilder();
			// reader = new StringReader(resource);
			InputSource input = new InputSource(new StringReader(resource));
			// Parse text to XML
			Document doc = builder.parse(input);
			// Parse OSM XML to MetroMap
			parseOsmXml(doc, map);
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private static void parseOsmXml(Document doc, final MetroMap metroMap) {
		// parse metrostations into a hashmap
		HashMap<String, MetroStation> metroStations = new HashMap<String, MetroStation>();
		HashMap<String, MetroStation> nonStationNodes = new HashMap<String, MetroStation>();
		NodeList stationNodes = doc.getElementsByTagName("node");
		for (int i = 0; i < stationNodes.getLength(); i++) {
			Node stationNode = stationNodes.item(i);

			NamedNodeMap stationAttributes = stationNode.getAttributes();
			// Get id, lng and lat attribute
			String nodeId = stationAttributes.getNamedItem("id").getNodeValue();
			float nodeLng = Float.parseFloat(stationAttributes.getNamedItem("lon").getNodeValue());
			float nodeLat = Float.parseFloat(stationAttributes.getNamedItem("lat").getNodeValue());

			// HACK: scale lat lng.
			nodeLng *= Math.cos(nodeLat / 180f * Math.PI);

			// Create MetroStation
			MetroStation m = new MetroStation(nodeLng, nodeLat, nodeId);
			
			// Separate stations from other nodes
			if (isStationNode(stationNode)) {
				m.label = getTagValue(stationNode, "name");
				metroStations.put(nodeId, m);
			} else {
				nonStationNodes.put(nodeId, m);
			}
		}

		// parse ways
		HashMap<String, HashMap<String, HashSet<String>>> edgeSets = new HashMap<String, HashMap<String, HashSet<String>>>();
		NodeList wayNodes = doc.getElementsByTagName("way");
		for (int i = 0; i < wayNodes.getLength(); i++) {
			Node wayNode = wayNodes.item(i);
			String wayId = ((Element)wayNode).getAttribute("id");
			HashMap<String, HashSet<String>> edgeSet = new HashMap<String, HashSet<String>>();
			edgeSets.put(wayId, edgeSet);
			// Iterate over node reference in way
			NodeList children = wayNode.getChildNodes();
			String prevRef = null;
			for (int j = 0; j < children.getLength(); j++) {
				Node child = children.item(j);
				if (child.getNodeType() == Node.ELEMENT_NODE) {
					Element childElement = (Element) child;
					if (childElement.getTagName().equals("nd")) {
						String ref = childElement.getAttribute("ref");
						
						if (prevRef != null) {
							// Add an edge from ref to prevRef
							if (edgeSet.containsKey(ref) == false)
								edgeSet.put(ref, new HashSet<String>());
							edgeSet.get(ref).add(prevRef);
							// Add an edge from prevRef to ref
							if (edgeSet.containsKey(prevRef) == false)
								edgeSet.put(prevRef, new HashSet<String>());
							edgeSet.get(prevRef).add(ref);
						}
						prevRef = ref;
					}
				}
			}
		}

		// Keep track of replaced stations.
		HashMap<String, String> replacedStations = new HashMap<String, String>();
		mergeStationsWithSameName(metroStations, edgeSets, replacedStations);
		//replacedStations.clear();
		
		// parse relations (metro lines) into MetroLines
		HashMap<String, MetroStation> metroStops = new HashMap<String, MetroStation>();
		NodeList relationNodes = doc.getElementsByTagName("relation");
		ArrayList<MetroLine> metroLines = new ArrayList<MetroLine>();
		// Iterate through relations
		for (int i = 0; i < relationNodes.getLength(); i++) {
			Node relationNode = relationNodes.item(i);
			if (relationNode.getNodeType() != Node.ELEMENT_NODE)
				continue;

			Element relationElement = (Element) relationNode;
			String relationId = relationElement.getAttribute("id");
			NodeList memberNodes = relationElement.getElementsByTagName("member");
			String color = getTagValue(relationNode, "colour");
			if (color == null)
				color = getTagValue(relationNode, "color");
			
			// Set of edges between stations and other non-station nodes. Edges are derived from ways.
			HashMap<String, HashSet<String>> edgeSet = new HashMap<String, HashSet<String>>();
			// Alternative station order derived from the sequence in which stations occur in the relation.
			ArrayList<MetroStation> relationStationOrder = new ArrayList<MetroStation>();
			// Set of stations
			HashMap<String, MetroStation> stationSet = new HashMap<String, MetroStation>();
			
			// Iterate through ways and nodes in this relation
			for (int j = 0; j < memberNodes.getLength(); j++) {
				Node memberNode = memberNodes.item(j);
				if (memberNode.getNodeType() == Node.ELEMENT_NODE) {
					Element memberElement = (Element) memberNode;
					String type = memberElement.getAttribute("type");
					String ref = memberElement.getAttribute("ref");
					String role = memberElement.getAttribute("role");
					if ("way".equals(type)) {
						if (edgeSets.containsKey(ref)) {
							HashMap<String, HashSet<String>> edges = edgeSets.get(ref);
							for (Map.Entry<String, HashSet<String>> kv : edges.entrySet()) {
								String k = kv.getKey();
								if (edgeSet.containsKey(k) == false)
									edgeSet.put(k, new HashSet<String>());
								edgeSet.get(k).addAll(kv.getValue());
							}
						}
						else
							System.out.println("Way ref " + ref + " not found from relation " + relationId);
					} else if ("node".equals(type)) {
						String ref2 = replacedStations.containsKey(ref) ? replacedStations.get(ref) : ref;
						if ("stop".equals(role) && metroStations.containsKey(ref2)) {
							// Line stops at this station
							MetroStation m = metroStations.get(ref2);
							stationSet.put(ref2, m);
							relationStationOrder.add(m);
						}
					}
				}
			}
			
			// Create ordered list of stations from station set and edges
			insertMissingStations(stationSet, nonStationNodes, edgeSet);
			removeNonStationEdges(stationSet, edgeSet);
			MetroLine metroLine = createMetroLine(stationSet, edgeSet);
			
			// Determine if metro line is probably loaded OK.
			boolean isCorrect = true;
			// Check 1: number of metro stations should be the same as the number of stops.
			if (metroLine.stations.length < stationSet.size()) {
				System.out.println("Relation " + relationId + " has too few stations. Using relation order.");
				isCorrect = false;
			}
			// Check 2: stations should not be much further than nearest neighbors.
			if (isCorrect) {
				float totalAdjDist = 0;
				float totalGeoDist = 0;
				for (int mi = 0; mi < metroLine.stations.length; mi++) {
					MetroStation m = metroLine.stations[mi];
					ArrayList<MetroStation> mAdj = new ArrayList<MetroStation>(2);
					if (mi > 0) mAdj.add(metroLine.stations[mi - 1]);
					if (mi < metroLine.stations.length - 1) mAdj.add(metroLine.stations[mi + 1]);
					for (MetroStation adj : mAdj) {
						float dx = (adj.geoX - m.geoX);
						float dy = (adj.geoY - m.geoY);
						totalAdjDist += Math.sqrt(dx * dx + dy * dy);
					}
					// Distances
					ArrayList<Float> dists = new ArrayList<Float>(metroLine.stations.length - 1);
					for (MetroStation m2 : metroLine.stations) {
						if (m2.ref != m.ref) {
							float dx = (m2.geoX - m.geoX);
							float dy = (m2.geoY - m.geoY);
							dists.add(dx * dx + dy * dy);
						}
					}
					// Sort and sum top distances
					Collections.sort(dists);
					List<Float> topDists = dists.subList(0, mAdj.size());
					for (Float f : topDists) {
						totalGeoDist += Math.sqrt(f);
					}
				}
				// Threshold
				if (totalAdjDist > 1.5f * totalGeoDist) {
					System.out.println("Relation " + relationId + " edges much longer than expected. Using relation order.");
					isCorrect = false;
				}
			}
			
			// If MetroLine is probably not correctly loaded from tracks, load from relation order.
			if (isCorrect == false) {
				metroLine = new MetroLine(relationStationOrder);
			}
			
			// Set additional attributes
			metroLine.ref = relationId;
			try {
			if (color != null)
				metroLine.color = ImportUtil.parseColor(color);
			} catch(NumberFormatException e) {
				System.out.println("Cannot parse color: '" + color + "'");
			}
			
			// Gather metro stations used in a metro line.
			for (int j = 0; j < metroLine.stations.length; j++) {
				MetroStation m = metroLine.stations[j];
				if (m.ref == null) {
					System.out.println("Station " + (m.label != null ? m.label : String.valueOf(m.id)) + " has no reference.");
				}
				if (metroStops.containsKey(m.ref) == false)
					metroStops.put(m.ref, m);
			}
			metroLines.add(metroLine);
		}

		// Sometimes lines are added in reverse, remove these metro lines.
		ArrayList<MetroLine> uniqueMetroLines = removeDuplicateLines(metroLines);
		
		// Remove lines with less than two stations.
		Iterator<MetroLine> it = uniqueMetroLines.iterator();
		while (it.hasNext()) {
			MetroLine l = it.next();
			if (l.stations.length < 2) {
				it.remove();
			}
		}
		
		// Create MetroMap from collections of stations and lines.
		metroMap.initialize(metroStops.values(), uniqueMetroLines);
	}

	private static boolean isStationNode(Node node) {
		String railway = getTagValue(node, "railway");
		return "station".equals(railway) || "halt".equals(railway); // || "subway_entrance".equals(railway);
	}

	private static String getTagValue(Node node, String key) {
		// Look through tags
		NodeList children = node.getChildNodes();
		for (int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);
			if (child.getNodeType() == Node.ELEMENT_NODE) {
				Element childElement = (Element) child;
				if (childElement.getTagName().equals("tag")) {
					String k = childElement.getAttribute("k");
					if (k.equals(key))
						return childElement.getAttribute("v");
				}
			}
		}

		return null;
	}
}
